package ijen.fbconnect.helpers;

import ijen.fbconnect.FBSession;
import org.apache.http.NameValuePair;
import org.apache.http.entity.mime.MultipartEntity;
import org.apache.http.entity.mime.content.StringBody;
import org.apache.http.entity.mime.content.FileBody;
import org.apache.http.message.BasicNameValuePair;

import java.io.UnsupportedEncodingException;
import java.io.File;
import java.net.URLEncoder;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.*;

/**
 * Description of UrlHelper.
 * ---------------------------------------------------------------------------------------
 * User: fred
 * Date: 16 juil. 2009
 * Time: 17:42:52
 * ---------------------------------------------------------------------------------------
 */
public final class FBParametersHelper {

    private static final String PARAM_FORMAT    = "%s=%s";
    private static final String SEP             = "&";
    private static final String UTF_8_ENCODING  = "UTF-8";

    private static final Map<String,String> DEFAULT_FB_PARAMS;

    private static final MessageDigest MD5;

    static {
        DEFAULT_FB_PARAMS = new HashMap<String, String>();
        DEFAULT_FB_PARAMS.put( "v",         "1.0" );    // Version
        DEFAULT_FB_PARAMS.put( "format",    "JSON" );   // Format

        try {
            MD5 = MessageDigest.getInstance("MD5");
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("Could not create MD5 object");
        }
    }


    /**
     * <p>Compute a GET request string for given map of parameters.
     * @param pParameters A map of key/value parameters
     * @return HTTP get request string
     */
    @Deprecated
    public String createParameterString(final Map<String,String> pParameters) {

        if (pParameters == null ) {     // Illegal argument
            throw new IllegalArgumentException("Parameters map can't be 'null'");
        }

        StringBuffer result = new StringBuffer();

        boolean oneShot = false;

        for (Map.Entry entry:pParameters.entrySet()) {

            if (oneShot) {
                result.append(SEP);
            } else {
                oneShot = true;
            }

            try {
                result.append(String.format(
                        PARAM_FORMAT,
                        URLEncoder.encode(entry.getKey().toString(),    UTF_8_ENCODING),      // Enforce URL encoding
                        URLEncoder.encode(entry.getValue().toString(),  UTF_8_ENCODING))      // on both key and value
                );
            } catch (UnsupportedEncodingException e) {
                throw new RuntimeException("Unable to encode key/value");
            }

        }

        return result.toString();
    }


    /**
     * <p>Convert a Map of 'generic' parameters into a list of HttpClient NameValuePair; easier for HTTP API calls.
     * @param pParameters A Map (string,string) of parameters
     * @return A list of NameValuePair.
     */
    public List<NameValuePair> getNameValuePairsFrom(final Map<String,String> pParameters) {

        if (pParameters == null ) {     // Illegal argument
            throw new IllegalArgumentException("Parameters map can't be 'null'");
        }

        List<NameValuePair> result = new ArrayList<NameValuePair>(pParameters.keySet().size());

        for (Map.Entry entry:pParameters.entrySet()) {
            result.add(new BasicNameValuePair(entry.getKey().toString(), entry.getValue().toString()));
        }

        return result;
    }


    /**
     * <p>Generate a signature for given parameters.
     * @param pParameters A map of key/value parameters
     * @param pSecret Secret seed
     * @return  Signtaure for API call
     */
    public String signature(final Map<String,String> pParameters, final String pSecret) {

        if (pParameters == null ) {     // Illegal argument
            throw new IllegalArgumentException("Parameters map can't be 'null'");
        }

        if (pSecret == null ) {         // Illegal argument
            throw new IllegalArgumentException("Secret can't be 'null'");
        }

        assert MD5 != null;

        StringBuffer result = new StringBuffer();

        List<String> keys = new ArrayList<String>(pParameters.keySet());    // Convert Set<T> into List<T> to sort ...
        Collections.sort(keys);     // According to FBSession, keys must be sorted alphabetically

        for (String key:keys) {     // Create a concatenated key/value sorted array
            if (!"".equals(key)) {   // Take care of special empty key needed for uploads !!!
                result.append( String.format("%s=%s", key, pParameters.get(key) ) );
            }
        }

        result.append(pSecret);     // Add secret as suffix

        return md5HexDigest(MD5.digest( result.toString().getBytes()));    // Return MD5 hexstring of result
    }


    /**
     * <p>Returns a hex string for given MD5 byte array.
     * <b>UNTESTED</>
     * @param pMd5Array MD5 byte array
     * @return Matching hex string
     */
    public String md5HexDigest(final byte[] pMd5Array) {

        if (pMd5Array == null) {     // Illegal argument
            throw new IllegalArgumentException("MD byte array can't be 'null'");
        }

        StringBuffer result = new StringBuffer();

        for (byte b:pMd5Array) {
            result.append(Integer.toString((b & 0xff) + 0x100, 16).substring(1));
        }

        return result.toString();
    }


    public void updateAndSignParameters(final FBSession pSession,
                                        final String pMethodName,
                                        final Map<String, String> pParameters) {
        assert pSession != null;
        assert pMethodName != null;
        assert pParameters != null;

        //Map<String,String> parameters = new HashMap<String, String>();

        pParameters.putAll(FBParametersHelper.DEFAULT_FB_PARAMS);      // Add default parameters
        pParameters.put("method", pMethodName);
        pParameters.put("api_key", pSession.getApiKey());

        if (!isSpecialMethod(pMethodName)) {
            pParameters.put("session_key", pSession.getSessionKey());
            pParameters.put("call_id", getCallId());

            if (pSession.getSessionSecret() != null) {
                pParameters.put("ss", "1") ;
                signParameters(pParameters, pSession.getSessionSecret());
            } else {
                signParameters(pParameters, pSession.getApplicationSecret());

            }

        } else {
            signParameters(pParameters, pSession.getApplicationSecret());
        }

    }


    /**
     * <p>Service classes MUST call getCallId') to get a proper callid for API calls instead of generating one in its own running thread.
     *
     * @return A call id based on time in millis.
     */
    public static synchronized String getCallId() {
        return String.format("%d", System.currentTimeMillis());
    }


    /**
     * <p>Sign a set of parameters. Generates the 'sig' parameter for Facebook API calls.
     * @param pParameters A list of parameters
     * @param pSecret Secret seed
     */
    public void signParameters(final Map<String, String> pParameters, final String pSecret) {

        if (pParameters == null) {
            throw new IllegalArgumentException("Can't sign: no parameters");
        }


        if (pSecret == null) {
            throw new IllegalArgumentException("Can't sign: invalid secret");
        }

        pParameters.put("sig", signature(pParameters, pSecret));   // Add signature
    }


    /**
     * <p>Generate of comma separated string from parameters.
     * <p>Useful for comma-separated parameters in API calls (uids for instance).
     * @param pTokens A set of tokens
     * @return A comma-sep'd string
     */
    public String commaSeparatedString(final Set<String> pTokens) {

        if (pTokens == null) {
            throw new IllegalArgumentException("Can't generate string from a null input");
        }

        StringBuffer result = new StringBuffer();

        final char sep = ',';
        boolean first = true;

        for (String s : pTokens) {

            if (first) {
                first = false;
            } else {
                result.append(sep);
            }

            result.append(s);
        }

        return result.toString();
    }


    /**
     * <p>Check whether method in parameters set is special.
     * <p>Special methods return incorrect JSON retrun object and result.
     * <p>Special methods are: 'facebook.auth.createToken' and 'facebook.auth.getSession'
     * @param pMethodName  API method name
     * @return True if method is one of the 2 'specials'
     */
    public boolean isSpecialMethod(final String pMethodName) {
        assert pMethodName != null;

        return "facebook.auth.createToken".equals(pMethodName) ||  "facebook.auth.getSession".equals(pMethodName);
    }


    /**
     * <p>This part encode Map parameters into multipart/form-data if needed.
     * <p>This is barely needed for uploading photos or docs.
     * @param pParameters Standard Map of key/value parameters
     * @return Multipart encoded parameters, directly usable by HttpPost class.
     */
    public MultipartEntity formatMultipartParameters(final Map<String, String> pParameters) {
        assert pParameters != null;

        MultipartEntity result = new MultipartEntity();

        for (Map.Entry entry:pParameters.entrySet()) {

            if ("".equals(entry.getKey().toString())) {             // Found the dreaded 'empty' special/upload key

                File f = new File(entry.getValue().toString());
                result.addPart(f.getName(), new FileBody(f) );

            }  else {
                try {
                    result.addPart(entry.getKey().toString(), new StringBody(entry.getValue().toString()) );
                } catch (UnsupportedEncodingException e) {
                    throw new RuntimeException("Could not encode multipart POST parameter: " + entry.getKey(), e);
                }
            }

        }


        return result;
    }

}